﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using FluentValidation;
using LINQPad.OpenAccess.Settings;
using LINQPad.OpenAccess.UI.Commands;
using LINQPad.OpenAccess.UI.Services;
using Telerik.OpenAccess.Config;

namespace LINQPad.OpenAccess.UI.ViewModel
{
    /// <summary>
    /// Connection settings view model for the Microsoft SQL Server.
    /// </summary>
    public class MsSqlConnectionSettingsViewModel : ConnectionSettingsViewModel<MsSqlConnectionSettingsViewModel>
    {
        /// <summary>
        /// The default description.
        /// </summary>
        public const string DefaultDescription = "Prepares a connection for the Microsoft SQL Server";
        /// <summary>
        /// The default name.
        /// </summary>
        public const string DefaultName = "SQL Server";

        #region Private

        #region - Vars

        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        private DelegateCommand<object> __loadAvailableDatabasesCommand;
        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        private string[] _availableDatabaseNames;
        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        private int _loadVersion;
        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        private string _serverName, _databaseName, _userName, _password, _availableDatabasesLoadFailedMessage;
        [DebuggerBrowsable( DebuggerBrowsableState.Never )]
        private bool _useWindowsAuthentication = true, _loadingAvailableDatabases;
        #endregion

        #region - Functions

        /// <summary>
        /// Called to actually get the available databases for the specified connection string.
        /// </summary>
        /// <returns>
        /// The available databases.
        /// </returns>
        private IList<string> DoGetAvailableDatabases( string connectionString )
        {
            var result = new List<string>( );
            if ( !String.IsNullOrWhiteSpace( connectionString ) )
            {
                using ( var sqlCon = new SqlConnection( connectionString ) )
                using ( var sqlCmd = sqlCon.CreateCommand( ) )
                {
                    sqlCon.Open( );
                    sqlCmd.CommandText = "SELECT name FROM sys.databases WHERE owner_sid <> 0x01 ORDER BY name";  // exclude system databases
                    using ( var reader = sqlCmd.ExecuteReader( ) )
                        while ( reader.Read( ) )
                            result.Add( reader.GetString( 0 ) );
                }
            }

            return result;
        }
        /// <summary>
        /// Handles the end of the 'LoadAvailableDatabaes' operation.
        /// </summary>
        /// <param name="task">The task.</param>
        /// <param name="version">The version.</param>
        private void EndLoadAvailableDatabases( Task<IList<string>> task, int loadVersion )
        {
            if ( this._loadVersion != loadVersion )
                return;

            this.LoadingAvailableDatabases = false;
            if ( task.IsFaulted )
            {
                task.Exception.Handle( e => true );
                this.AvailableDatabaseNames = null;
                this.AvailableDatabasesLoadFailedMessage = task.Exception.InnerException.Message;
            }
            else
            {
                this.AvailableDatabaseNames = task.Result.ToArray( );
                this.AvailableDatabasesLoadFailedMessage = null;
            }
        }
        /// <summary>
        /// Sets the name of the server.
        /// </summary>
        /// <param name="serverName">Name of the server.</param>
        /// <param name="loadAvailableDatabases">if set to <c>true</c> to load available the databases.</param>
        private void SetServerName( string serverName, bool loadAvailableDatabases = true )
        {
            if ( this._serverName != serverName )
            {
                this._serverName = serverName;
                this.OnPropertyChanged( ( ) => this.ServerName );
                this.RaiseCanExecuteChangedForCommands( );
                if ( loadAvailableDatabases )
                    this.LoadAvailableDatabasesAsync( );
                this.Invalidate( );
            }
        }
        #endregion

        #endregion

        #region Protected

        #region - Properties

        /// <summary>
        /// Gets a value indicating whether the view model is currently able to load the available databases.
        /// </summary>
        /// <value>
        /// <c>true</c> if the view model is currently able to load the available databases; otherwise, <c>false</c>.
        /// </value>
        protected virtual bool CanLoadAvailableDatabases
        {
            get
            {
                return this.DatabaseInfo != null
                        && this.DriverSettings != null
                        && !String.IsNullOrWhiteSpace( this._serverName )
                        && ( this._useWindowsAuthentication
                            || ( !String.IsNullOrWhiteSpace( this._userName ) && !String.IsNullOrWhiteSpace( this._password ) ) );
            }
        }

        #endregion

        #endregion

        #region Public

        #region - Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="MsSqlConnectionSettingsViewModel" /> class.
        /// </summary>
        /// <param name="validator">The validator.</param>
        public MsSqlConnectionSettingsViewModel( IUserInteractionService userInteractionService, IValidator<MsSqlConnectionSettingsViewModel> validator )
            : base( DefaultName, DefaultDescription, userInteractionService, validator )
        {
            this._availableDatabaseNames = new string[0];
        }

        #endregion

        #region - Functions

        /// <summary>
        /// Loads the available databases in an async fashion.
        /// </summary>
        public void LoadAvailableDatabasesAsync( )
        {
            var loadVersion = Interlocked.Increment( ref _loadVersion );
            if ( !this.CanLoadAvailableDatabases )
            {
                this.LoadingAvailableDatabases = false;
                this.AvailableDatabasesLoadFailedMessage = null;
                this.AvailableDatabaseNames = null;
                return;
            }
            else
            {
                this.DoWriteSettings( );
                var connectionString = this.DatabaseInfo.GetCxString( );

                this.LoadingAvailableDatabases = true;
                Task.Factory
                    .StartNew( ( ) => this.DoGetAvailableDatabases( connectionString ), CancellationToken.None, TaskCreationOptions.AttachedToParent, TaskScheduler.Default )
                    .ContinueWith( t => EndLoadAvailableDatabases( t, loadVersion ), CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Current );
            }
        }

        #endregion

        #region - Commands

        /// <summary>
        /// Gets the load available databases command.
        /// </summary>
        /// <value>
        /// The load available databases command.
        /// </value>
        public ICommand LoadAvailableDatabasesCommand
        {
            get
            {
                return this.__loadAvailableDatabasesCommand ?? ( this.__loadAvailableDatabasesCommand = new DelegateCommand<object>( _ => LoadAvailableDatabasesAsync( ), _ => this.CanLoadAvailableDatabases ) );
            }
        }

        #endregion

        #region - Properties

        /// <summary>
        /// Gets the available database names.
        /// </summary>
        /// <value>
        /// The available database names.
        /// </value>
        public string[] AvailableDatabaseNames
        {
            get
            {
                return this._availableDatabaseNames;
            }
            private set
            {
                if ( this._availableDatabaseNames != value )
                {
                    this._availableDatabaseNames = value ?? new string[0];
                    this.OnPropertyChanged( ( ) => this.AvailableDatabaseNames );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether the available databases could not be loaded.
        /// </summary>
        /// <value>
        /// <c>true</c> if the available databases could not be loaded; otherwise, <c>false</c>.
        /// </value>
        public bool AvailableDatabasesLoadFailed
        {
            get
            {
                return !String.IsNullOrWhiteSpace( this._availableDatabasesLoadFailedMessage );
            }
        }
        /// <summary>
        /// Gets the available databases load failed message.
        /// </summary>
        /// <value>
        /// The available databases load failed message.
        /// </value>
        public string AvailableDatabasesLoadFailedMessage
        {
            get
            {
                return this._availableDatabasesLoadFailedMessage;
            }
            private set
            {
                if ( this._availableDatabasesLoadFailedMessage != value )
                {
                    this._availableDatabasesLoadFailedMessage = value;
                    this.OnPropertyChanged( ( ) => AvailableDatabasesLoadFailed );
                    this.OnPropertyChanged( ( ) => this.AvailableDatabasesLoadFailedMessage );
                }
            }
        }
        /// <summary>
        /// Gets or sets the name of the database.
        /// </summary>
        /// <value>
        /// The name of the database.
        /// </value>
        public string DatabaseName
        {
            get
            {
                return this._databaseName;
            }
            set
            {
                if ( this._databaseName != value )
                {
                    this._databaseName = value;
                    this.OnPropertyChanged( ( ) => this.DatabaseName );
                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether the available databases are currently loaded.
        /// </summary>
        /// <value>
        /// <c>true</c> if the available databases are currently loaded; otherwise, <c>false</c>.
        /// </value>
        public bool LoadingAvailableDatabases
        {
            get
            {
                return this._loadingAvailableDatabases;
            }
            private set
            {
                if ( this._loadingAvailableDatabases != value )
                {
                    this._loadingAvailableDatabases = value;
                    this.OnPropertyChanged( ( ) => this.LoadingAvailableDatabases );
                }
            }
        }
        /// <summary>
        /// Gets or sets the password.
        /// </summary>
        /// <value>
        /// The password.
        /// </value>
        public string Password
        {
            get
            {
                return this._password;
            }
            set
            {
                if ( this._password != value )
                {
                    this._password = value;
                    this.OnPropertyChanged( ( ) => this.Password );
                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets or sets the name of the server.
        /// </summary>
        /// <value>
        /// The name of the server.
        /// </value>
        public string ServerName
        {
            get
            {
                return this._serverName;
            }
            set
            {
                this.SetServerName( value, loadAvailableDatabases: true );
            }
        }
        /// <summary>
        /// Gets or sets the name of the user.
        /// </summary>
        /// <value>
        /// The name of the user.
        /// </value>
        public string UserName
        {
            get
            {
                return this._userName;
            }
            set
            {
                if ( this._userName != value )
                {
                    this._userName = value;
                    this.OnPropertyChanged( ( ) => this.UserName );
                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets or sets a value indicating whether windows authentication should be used.
        /// </summary>
        /// <value>
        /// <c>true</c> if windows authentication should be used; otherwise, <c>false</c>.
        /// </value>
        public bool UseWindowsAuthentication
        {
            get
            {
                return this._useWindowsAuthentication;
            }
            set
            {
                if ( this._useWindowsAuthentication != value )
                {
                    this._useWindowsAuthentication = value;
                    this.OnPropertyChanged( ( ) => this.UseWindowsAuthentication );
                    // for validation purposes
                    this.OnPropertyChanged( ( ) => this.UserName );
                    this.OnPropertyChanged( ( ) => this.Password );
                    this.Invalidate( );
                }
            }
        }

        #endregion

        #endregion

        #region ConnectionSettingsViewModel<MsSqlConnectionSettingsViewModel> Members

        /// <summary>
        /// Determines whether the specified provider is supported.
        /// </summary>
        /// <param name="providerName">Name of the provider.</param>
        /// <returns>
        ///   <c>true</c> if the specified provider is supported; otherwise, <c>false</c>.
        /// </returns>
        public override bool IsProviderSupported( string providerName )
        {
            return providerName == ConfigDefs.SqlClient;
        }
        /// <summary>
        /// Refreshes this instance.
        /// </summary>
        public override void Refresh( )
        {
            this.LoadAvailableDatabasesAsync( );
        }
        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose( bool disposing )
        {
            if ( disposing )
            {
                this.DisposeField( ref this.__loadAvailableDatabasesCommand );
            }
            base.Dispose( disposing );
        }
        /// <summary>
        /// Called to actually load the view model's settings from the specified sources.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="driverSettings">The driver settings.</param>
        protected override void DoLoadSettings( )
        {
            var databaseInfo = this.DatabaseInfo;
            this.SetServerName( databaseInfo.Server, loadAvailableDatabases: false );
            this.UseWindowsAuthentication = !databaseInfo.SqlSecurity;
            this.Password = databaseInfo.Password;
            this.UserName = databaseInfo.UserName;
            this.DatabaseName = databaseInfo.Database;

            this.LoadAvailableDatabasesAsync( );
        }
        /// <summary>
        /// Called to actually writes the view model's settings to the specified targets.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="driverSettings">The driver settings.</param>
        protected override void DoWriteSettings( )
        {
            this.DriverSettings.Connection = new ConnectionSettings( );
            var databaseInfo = this.DatabaseInfo;
            databaseInfo.Server = this.ServerName;
            databaseInfo.SqlSecurity = !this.UseWindowsAuthentication;
            databaseInfo.Password = this.UseWindowsAuthentication ? null : this.Password;
            databaseInfo.UserName = this.UseWindowsAuthentication ? null : this.UserName;
            databaseInfo.Database = this.DatabaseName;
        }
        /// <summary>
        /// Raises the can execute changed for commands.
        /// </summary>
        protected override void RaiseCanExecuteChangedForCommands( )
        {
            base.RaiseCanExecuteChangedForCommands( );
            if ( this.__loadAvailableDatabasesCommand != null )
                this.__loadAvailableDatabasesCommand.RaiseCanExecuteChanged( );
        }
        #endregion
    }
}
